/*
 * ESLoader.java - ScriptME
 * 
 * Copyright (c) 2009 Cesar Henriques <cesar at alttab.com.ar>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Based on FESI Project
 * 
 * Contributors:
 * 	Jean-Marc Lugrin - initial API and implementation
 * 	Cesar Henriques <cesar at alttab.com.ar> - J2ME Porting and Extensions
 */
package org.scriptme.ecmascript.data;

import java.util.Date;

import org.scriptme.ecmascript.exceptions.EcmaScriptException;
import org.scriptme.ecmascript.exceptions.ProgrammingError;
import org.scriptme.ecmascript.interpreter.Evaluator;
import org.scriptme.ecmascript.interpreter.LocalClassLoader;
import org.scriptme.ecmascript.interpreter.ScopeChain;
import org.scriptme.ecmascript.jslib.JSFunction;

/**
 * Implements the common functionality of package(object) and beans loaders.
 */
public abstract class ESLoader extends ESObject {

	// Debug support
	/** The debug java access. */
	static protected boolean debugJavaAccess = false;

	/**
	 * Sets the debug java access.
	 * 
	 * @param b
	 *            the new debug java access
	 */
	public static void setDebugJavaAccess(boolean b) {
		debugJavaAccess = b;
	}

	/**
	 * Checks if is debug java access.
	 * 
	 * @return true, if is debug java access
	 */
	static public boolean isDebugJavaAccess() {
		return debugJavaAccess;
	}

	/** The debug loader. */
	static protected boolean debugLoader = false;

	/**
	 * Sets the debug loader.
	 * 
	 * @param b
	 *            the new debug loader
	 */
	public static void setDebugLoader(boolean b) {
		debugLoader = b;
	}

	/**
	 * Checks if is debug loader.
	 * 
	 * @return true, if is debug loader
	 */
	static public boolean isDebugLoader() {
		return debugLoader;
	}

	// Incremental package name
	/** The package name. */
	protected String packageName = null;

	/** The previous package. */
	protected ESLoader previousPackage = null;

	/** The class loader. */
	protected LocalClassLoader classLoader = null;

	// the non compatible flag
	/** The non compatible. */
	static private CompatibilityDescriptor nonCompatible = new CompatibilityDescriptor(
			-1, null, null);

	/**
	 * To contruct the Bean or Package object.
	 * 
	 * @param evaluator
	 *            the evaluator
	 */
	public ESLoader(Evaluator evaluator) {
		super(null, evaluator);
	}

	/**
	 * To construct a bean or package sub-object (with a specific partial or
	 * complete package name.
	 * 
	 * @param packageName
	 *            The extension of the package name
	 * @param previousPackage
	 *            Represents the higher level package names
	 * @param classLoader
	 *            the class loader to use for this loader
	 * @param evaluator
	 *            the evaluator
	 */
	public ESLoader(String packageName, ESLoader previousPackage,
			LocalClassLoader classLoader, Evaluator evaluator) {
		super(null, evaluator);
		this.packageName = packageName;
		this.previousPackage = previousPackage;
		this.classLoader = classLoader;
	}

	/**
	 * Build the prefix name of the package, concatenating all upper level
	 * prefix separated by dots.
	 * 
	 * @return prefix of the current package name
	 */
	protected String buildPrefix() {
		if (previousPackage == null) {
			return null;
		} else {
			String prefix = previousPackage.buildPrefix();
			if (prefix == null) {
				return packageName;
			} else {
				return prefix + "." + packageName;
			}
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getPrototype()
	 */
	public ESObject getPrototype() {
		throw new ProgrammingError("Cannot get prototype of Package");
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getTypeOf()
	 */
	public int getTypeOf() {
		return EStypeObject;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getPropertyInScope(java.lang.String,
	 *      org.scriptme.ecmascript.interpreter.ScopeChain, int)
	 */
	public ESValue getPropertyInScope(String propertyName,
			ScopeChain previousScope, int hash) throws EcmaScriptException {
		throw new EcmaScriptException("A loader object (" + this
				+ ") should not be part of a with statement");
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#hasProperty(java.lang.String,
	 *      int)
	 */
	public boolean hasProperty(String propertyName, int hash)
			throws EcmaScriptException {
		return true; // So it can be dereferenced by scopechain
		// and wont be created
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#isHiddenProperty(java.lang.String,
	 *      int)
	 */
	public boolean isHiddenProperty(String propertyName, int hash) {
		return false;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#putProperty(java.lang.String,
	 *      org.scriptme.ecmascript.data.ESValue, int)
	 */
	public void putProperty(String propertyName, ESValue propertyValue, int hash)
			throws EcmaScriptException {
		return; // None can be put by the user
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#putHiddenProperty(java.lang.String,
	 *      org.scriptme.ecmascript.data.ESValue)
	 */
	public void putHiddenProperty(String propertyName, ESValue propertyValue)
			throws EcmaScriptException {
		throw new ProgrammingError("Cannot put hidden property in " + this);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#deleteProperty(java.lang.String,
	 *      int)
	 */
	public boolean deleteProperty(String propertyName, int hash)
			throws EcmaScriptException {
		// all possible package name do potentialy exists and
		// cannot be deleted, as they are recreated at the first
		// reference.
		return false;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getDefaultValue(int)
	 */
	public ESValue getDefaultValue(int hint) throws EcmaScriptException {
		if (hint == EStypeString) {
			return new ESString(this.toString());
		} else {
			throw new EcmaScriptException("No default value for " + this
					+ " and hint " + hint);
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getDefaultValue()
	 */
	public ESValue getDefaultValue() throws EcmaScriptException {
		return this.getDefaultValue(EStypeString);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#doConstruct(org.scriptme.ecmascript.data.ESObject,
	 *      org.scriptme.ecmascript.data.ESValue[])
	 */
	public ESObject doConstruct(ESObject thisObject, ESValue[] arguments)
			throws EcmaScriptException {
		throw new EcmaScriptException("No contructor for loader object: "
				+ this);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#doubleValue()
	 */
	public double doubleValue() {
		double d = Double.NaN;
		return d;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#booleanValue()
	 */
	public boolean booleanValue() {
		return true;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#toString()
	 */
	public String toString() {
		return this.toDetailString();
	}

	// ---------------------------------------------------------------
	// Tools for the java wrapper objects
	// ---------------------------------------------------------------

	/**
	 * Returns true if it is a primitive type.
	 * 
	 * @param cls
	 *            the cls
	 * 
	 * @return true if primitive type
	 */
	static boolean isBasicClass(Class cls) {
		return cls == String.class || cls == Character.class
				|| cls == Byte.class || cls == Short.class
				|| cls == Integer.class || cls == Long.class
				|| cls == Float.class || cls == Double.class
				|| cls == Boolean.class || cls == Date.class;
	}

	/**
	 * Checks if is primitive number class.
	 * 
	 * @param cls
	 *            the cls
	 * 
	 * @return true, if is primitive number class
	 */
	static boolean isPrimitiveNumberClass(Class cls) {
		return cls == Byte.class || cls == Short.class || cls == Integer.class
				|| cls == Long.class || cls == Float.class
				|| cls == Double.class;
	}

	/**
	 * Transform a java object to an EcmaScript value (primitive if possible).
	 * 
	 * @param obj
	 *            the object to transform
	 * @param evaluator
	 *            the evaluator
	 * 
	 * @return the EcmaScript object
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 * 
	 * @exception EcmaScriptException
	 *                the normalization failed
	 */
	public static ESValue normalizeValue(Object obj, Evaluator evaluator)
			throws EcmaScriptException {
		if (obj == null) {
			return ESNull.theNull;
		} else if (obj instanceof String) {
			return new ESString((String) obj);
		} else if (isPrimitiveNumberClass(obj.getClass())) {
			return new ESNumber(Double.parseDouble(obj.toString()));
		} else if (obj instanceof Boolean) {
			return ESBoolean.makeBoolean(((Boolean) obj).booleanValue());
		} else if (obj instanceof Character) {
			return new ESNumber(((Character) obj).charValue());
		} else if (obj instanceof JSFunction) {
			return JSWrapper.wrapJSFunction(evaluator, (JSFunction) obj);
		} else if (obj instanceof JSWrapper) {
			return ((JSWrapper) obj).getESObject();
		} else if (obj instanceof Date) {
			return new DatePrototype(evaluator, (Date) obj);
		} else if (obj instanceof ESWrapper) {
			return (ESWrapper) obj; // A wrapper received externally
		} else if (obj instanceof ESArrayWrapper) {
			return (ESArrayWrapper) obj; // An array wrapper received
											// externally
		} else if (obj.getClass().isArray()) {
			return new ESArrayWrapper(obj, evaluator);
		}
		return new ESWrapper(obj, evaluator);
	}

	/**
	 * Transform a java object to an EcmaScript object (not a primitive).
	 * 
	 * @param obj
	 *            the object to transform
	 * @param evaluator
	 *            the evaluator
	 * 
	 * @return the EcmaScript object
	 * 
	 * @throws EcmaScriptException
	 *             the ecma script exception
	 * 
	 * @exception EcmaScriptException
	 *                the normalization failed
	 */
	public static ESObject normalizeObject(Object obj, Evaluator evaluator)
			throws EcmaScriptException {
		ESValue value = normalizeValue(obj, evaluator);
		return (ESObject) value.toESObject(evaluator);
	}

	/**
	 * Convert the primitive class types to their corresponding class types.
	 * Must be called for primitive classes only.
	 * 
	 * @param target
	 *            The primitive class to convert
	 * 
	 * @return The converted class
	 */
	private static Class convertPrimitive(Class target) {
		return target;
		/*
		 * if (target==java.lang.Boolean.TYPE) { target=Boolean.class; } else if
		 * (target==java.lang.Character.TYPE) { target=Character.class; } else
		 * if (target==java.lang.Byte.TYPE) { target=Byte.class; } else if
		 * (target==java.lang.Short.TYPE) { target=Short.class; } else if
		 * (target==java.lang.Integer.TYPE) { target=Integer.class; } else if
		 * (target==java.lang.Long.TYPE) { target=Long.class; } else if
		 * (target==java.lang.Float.TYPE) { target=Float.class; } else if
		 * (target==java.lang.Double.TYPE) { target=Double.class; } else { throw
		 * new ProgrammingError("Not a recognized primitive type: " + target); }
		 * return target;
		 */
	}

	/**
	 * Get a number correlated to the wideness of the class in some lousy sense.
	 * 
	 * @param cls
	 *            the cls
	 * 
	 * @return the number size
	 */
	static private int getNumberSize(Class cls) {

		if (cls == Byte.class) {
			return 1;
		} else if (cls == Character.class) {
			return 2;
		} else if (cls == Short.class) { // short and char widen in the same
											// way
			return 2;
		} else if (cls == Integer.class) {
			return 3;
		} else if (cls == Long.class) {
			return 4;
		} else if (cls == Float.class) {
			return 5;
		} else if (cls == Double.class) {
			return 6;
		} else {
			throw new ProgrammingError("Unexpected number class");
		}
	}

	/**
	 * Check that each object in the paremeter array can be converted to the
	 * type specified by the target array and convert them if needed.
	 * <P>
	 * Even if the parameters are compatible with an EcmaScript value, some may
	 * have to be converted to an intermediate type. For example an EcmaScript
	 * string of 1 character long is compatible with a Java Character, but some
	 * conversion is needed. Arrays need a similar special processing.
	 * <P>
	 * The parameters have been converted to java Objects by the routine
	 * toJavaObject. Wrapped objects (java objects given to an EcmaScript
	 * routine and given back as parameters) have been unwrapped, so they are
	 * their original object again (including arrays), we do therefore not have
	 * ESWrapped objects as parameters. ESObjects have been wrapped in a
	 * JSObject, including Array objects. The handling of array is more delicate
	 * as they could not be converted to a cannonical form - we must know the
	 * element type to understand if they are convertible.
	 * <P>
	 * The target classes which are primitive are converted to their object
	 * counterpart, as only object can be given as parameter and the invoke
	 * mechanism will convert them back to primitive if needed.
	 * <P>
	 * All the conversion needed are described in the returned
	 * compatibilityDescriptor and will only be applied for the selected
	 * routine.
	 * <P>
	 * The distance is a metric on how "far" a parameter list is from the
	 * immediate value (used currntly only for simple value widening). It allows
	 * to select the routine having the nearest parameter list. This is not
	 * totally full proof and multiple routine can have the same distance
	 * (including 0, because of the conversion of primitive type to the
	 * corresponding objects).
	 * <P>
	 * The tracing must allow to find the logic of the conversion.
	 * 
	 * @param target
	 *            The class objects of the target routine
	 * @param params
	 *            the params
	 * 
	 * @return a compatibility descriptor.
	 */
	static CompatibilityDescriptor areParametersCompatible(Class target[],
			Object params[]) {
		int n = target.length;
		if (n != params.length)
			return nonCompatible; // Ensure we have the same number
		boolean[] convertToChar = null; // flag to indicate if conversion to
										// char is needed
		Object[] convertedArrays = null; // Converted array if any needed
		int distance = 0; // For perfect match, something added for widening
		for (int i = 0; i < n; i++) {
			boolean accepted = false;
			Class targetClass = target[i];
			Class sourceClass = null;
			String debugInfo = " not accepted";
			if (params[i] == null) {
				// A null parameter is of type Object, so we check
				// that whatever is the target class be an object
				if (isPrimitive(targetClass)) {
					// or: Object.class.isAssignableFrom(targetClass)
					accepted = false;
					debugInfo = " rejected (null cannot be assigned to primitive)";
				} else {
					accepted = true;
					debugInfo = " accepted (null to Object)";
				}
			} else {
				// We consider all primitive types as they object
				// equivallent, as the parameter can only be done as
				// object anyhow. Invoke will convert back if needed.
				if (isPrimitive(targetClass)) {
					// To accept long by Long, etc... - must be done after test
					// of assigning null to object
					targetClass = convertPrimitive(targetClass);
				}
				// The simplest case is direct object compatibility
				sourceClass = params[i].getClass();
				accepted = targetClass.isAssignableFrom(sourceClass);
				debugInfo = " accepted (subclassing)";

				if (!accepted) {
					// If we do not have direct object compatibility, we check
					// various
					// allowed conversions.

					// Handle number and number widening
					if ((isPrimitiveNumberClass(sourceClass) || sourceClass == Character.class)
							&& isPrimitiveNumberClass(targetClass)) {
						// Can be widened ?
						int targetSize = getNumberSize(targetClass);
						int sourceSize = getNumberSize(sourceClass);
						if (targetSize > sourceSize) {
							accepted = true; // if == already accepted
												// because same class
							// or must be rejected (because char != short)
							distance += Math.abs(targetSize - sourceSize);
							debugInfo = " accepted (number widening: "
									+ distance + ")";
						} else {
							debugInfo = " rejected (not widening numbers)";
						}
						// Handle String of length 1 as a Char, which can be
						// converted to a number
					} else if ((targetClass == Character.class
							|| targetClass == Integer.class
							|| targetClass == Long.class
							|| targetClass == Float.class || targetClass == Double.class)
							&& params[i] instanceof String) {
						if (((String) params[i]).length() == 1) {
							accepted = true; // will require conversion of
												// parameter
							if (convertToChar == null) {
								convertToChar = new boolean[n];
							}
							convertToChar[i] = true;
							debugInfo = " accepted (String(1) as Character)";
						} else {
							debugInfo = " rejected (String not of length 1)";
						}

						// Handle array conversion if not from a native java
						// array
					} else if (targetClass.isArray()) {
						if (params[i] instanceof JSWrapper) {
							ESObject esArray = ((JSWrapper) params[i])
									.getESObject();
							if (esArray instanceof ArrayPrototype) {
								Object array = null;
								// We convert to the orginal class (possibly a
								// primitive type)
								/*
								 * array = ((ArrayPrototype)
								 * esArray).toJavaArray(targetClass.getComponentType());
								 * accepted = true; debugInfo = " accepted
								 * (array converted)"; if (convertedArrays ==
								 * null) { convertedArrays = new Object[n]; }
								 */
								convertedArrays[i] = array; // save it for
															// replacement at
															// end
							} else {
								debugInfo = " rejected (EcmaScript object is not an array)";
							}
						} else {
							debugInfo = " rejected (only same native array or EcmaScript Array can be assigned to array)";
						}

					} else {
						debugInfo = " rejected (incompatible types)";
					}
				} // if ! acccepted
			} // if ! null

			if (debugJavaAccess)
				System.out.println(" Assign " + sourceClass + " to "
						+ target[i] + debugInfo);
			if (!accepted)
				return nonCompatible;

		} // for

		// Compatible, return appropriate descriptor for future
		// processing of conversion of the "nearest" method
		return new CompatibilityDescriptor(distance, convertToChar,
				convertedArrays);
	}

	/**
	 * Checks if is primitive.
	 * 
	 * @param t
	 *            the t
	 * 
	 * @return true, if is primitive
	 */
	private static boolean isPrimitive(Class t) {
		try {
			if (t.isInstance(Byte.class.newInstance()))
				return true;

			if (t.isInstance(Character.class.newInstance()))
				return true;

			if (t.isInstance(Short.class.newInstance()))
				return true;

			if (t.isInstance(Integer.class.newInstance()))
				return true;

			if (t.isInstance(Long.class.newInstance()))
				return true;

			if (t.isInstance(Float.class.newInstance()))
				return true;

			if (t.isInstance(Double.class.newInstance()))
				return true;
		} catch (IllegalAccessException ex) {
			ex.printStackTrace();
		} catch (InstantiationException ex) {
			ex.printStackTrace();
		}

		return false;
	}

	// overrides
	/**
	 * Type name.
	 * 
	 * @param t
	 *            the t
	 * 
	 * @return the string
	 */
	static public String typeName(Class t) {
		String brackets = "";
		while (t.isArray()) {
			brackets += "[]";
			t = t.getClass();
		}
		return t.getName() + brackets;
	}
}
